上面的 gif 演示了這個圖床網站的運作方式。
當我們點擊某一張圖片時可以看到一個 modal 彈出來,顯示我們點擊的圖片。此時可以注意到網址列已經改變,當重新整理時會看到網頁的結果跟一開始點擊時不一樣,它不是以 modal 的方式顯示,而且它已經脫離首頁,是單頁的圖片介紹。
這就是 intercepting routes 常見的使用方式。
當在網站內作路由導航時,可以攔截這個導航,讓我們在不脫離原本所在頁面的情況下,顯示另一個頁面的部分資訊。
當我們直接訪問這個網址(硬導航、重新整理或訪問這個網址),那就不會被攔截,而是直接顯示該網址原本應該顯示的內容。
intercepting routes 是基於軟導航(soft navigation)的應用,每個軟導航都會通過 Next 建立的 middleware。
這個 middleware 其中一個作用就是查看該網址需不需要攔截下來,需要的話網址一樣會更改,但不是直接訪問到該頁面,而是吐出我們另外寫的,對應該頁面的 UI。
通常都會是 Modal 彈窗,否則該 UI 會基於原頁面直接往下長,看起來會蠻奇怪的。
建立 intercepting routes 需要建立 (..)
系列的保留字建立檔案,這裡的 ..
跟相對路徑的概念很像,一個點代表要 match 同一個 level 的對應路由,兩個點代表要配對上一層 level 的對應路由。
括號點的規則:
(.)
:match 同一層 route(..)
:match 上一層 route(..)(..)
:match 上兩層 route(...)
:match 根目錄 route我們建立的 intercepting route 需要對應一個真實存在的路由。
以上面來說建立 app/feed/(..)photo/[id]/page.tsx
這個 intercepting route 以前,應該要先有 app/photo/[id]/page.tsx
這個真實存在的路由。
當建立好這個 intercepting route 以後,只要是軟導航到 /photo/[id]
,就會吐出我們建立的 app/feed/(..)photo/[id]/page.tsx
。
而硬導航、重新整理或直接訪問 /photo/[id]
則會直接顯示原頁面的 UI (app/photo/[id]/page.tsx
)。
這裡講的 match route 以資料夾來看,只會算進對 route 產生影響的資料夾。
像是前幾天講到的 route group(groupName)
或是 parallel routes@folder
,這類不會對實際對路由造成影響的資料夾,不會算作一層
開頭的圖床網站還有一個功能沒有講到,圖片 modal 彈出來後,可以點擊 modal 周圍的 overlay 或 eac,路由會退回原本訪問的頁面(modal 也會因為路由退回去而消失)。
這就需要使用 useRouter
裡提供的 back
method 回到上一頁,這是一個 react hook,所以只能在 client component 中使用。
底下是一個官方的 Modal 組件範例,這裡的 onDissmiss
使用 Next 提供的 useRouter
作出回到原頁面的動作,按下 esc
或是點擊 modal 周圍的 overlay 時會觸發。
// client component
'use client'
import { useCallback, useRef, useEffect, MouseEventHandler } from 'react'
import { useRouter } from 'next/navigation'
export default function Modal({ children }: { children: React.ReactNode }) {
const overlay = useRef(null)
const wrapper = useRef(null)
const router = useRouter()
const onDismiss = useCallback(() => {
router.back()
}, [router])
const onClick: MouseEventHandler = useCallback(
(e) => {
if (e.target === overlay.current || e.target === wrapper.current) {
if (onDismiss) onDismiss()
}
},
[onDismiss, overlay, wrapper]
)
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') onDismiss()
},
[onDismiss]
)
useEffect(() => {
document.addEventListener('keydown', onKeyDown)
return () => document.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
return (
<div
ref={overlay}
className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60"
onClick={onClick}
>
<div
ref={wrapper}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full sm:w-10/12 md:w-8/12 lg:w-1/2 p-6"
>
{children}
</div>
</div>
)
}